package com.tools.web.interceptor.limiting;

import com.tools.common.object.Note;
import com.tools.common.thread.Timeunit;
import com.tools.web.WebKit;
import com.tools.web.interceptor.auth.AuthenticationManager;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

import java.lang.reflect.Method;

/**
 * 请求限流拦截器
 * 可以限制请求的次数。
 * 同时若满足三个条件则可以开启封号功能：
 *      1.useAuthHandler 为 true。
 *      2.AuthenticationManager 实例不为 null
 *      3.目标接口的 @RequestLimit 注解的 maxAllow 大于等于 allow 的值
 * 开启功能后，若请求次数大于等于 maxAllow，则执行封禁当前用户的账号
 * */
@Note("请求限流拦截器。" +
        "可以限制请求的次数。同时若满足三个条件则可以开启封号功能：" +
        "     1.useAuthHandler 为 true。" +
        "     2.AuthenticationManager 实例不为 null" +
        "     3.目标接口的 @RequestLimit 注解的 maxAllow 大于等于 allow 的值" +
        "开启功能后，若请求次数大于等于 maxAllow，则执行封禁当前用户的账号")
public abstract class RequestLimitInterceptor implements HandlerInterceptor {

    @Note("请求次数存储在存储里的 key 的前缀")
    private final String cachePrefix;

    @Note("请求次数达到上限后给客户端响应的信息内容。最好是 JSON 字符串")
    private final String limitResponseMessage;

    @Note("若开启了 【请求次数达到最大上限就封禁账户的功能】，该接口实例的方法就是执行封号的功能逻辑。" +
            "这也是开关之一。若 useAuthHandler 为 true 但该实例为 null 也不会启用该功能。" +
            "还有的开关是在接口注解出，maxAllow 需要大于等于 allow。" +
            "三个条件都满足才会启用封号功能")
    private AuthenticationManager authHandler;

    @Note("封禁账号后，这里决定响应给客户端什么内容，可以为空，为空则不返回内容")
    private String disabledResponseMessage;


    /* **************************************************************************************
     *
     *          构造器
     *
     * **************************************************************************************
     * */

    public RequestLimitInterceptor(RequestLimitSource source, boolean mustTrue, String errorMessage) {
        this(source, mustTrue, errorMessage, null);
    }

    public RequestLimitInterceptor(RequestLimitSource source, boolean mustTrue, String errorMessage,
                                   AuthenticationManager authenticationManager) {
        if(!mustTrue) throw new IllegalArgumentException(errorMessage);
        String limitResponseMessage = source.getLimitResponseMessage();
        if(limitResponseMessage == null || limitResponseMessage.isEmpty()) throw new NullPointerException("已被限流应当响应给客户端的信息为空");
        this.limitResponseMessage = limitResponseMessage;
        String cachePrefix = source.getCachePrefix();
        if(cachePrefix != null && !cachePrefix.isEmpty()) this.cachePrefix = cachePrefix;
        else this.cachePrefix = "limit:req:";
        if(authenticationManager != null) {
            String disabledResponseMessage = source.getDisabledResponseMessage();
            if(disabledResponseMessage == null || disabledResponseMessage.isEmpty()) throw new NullPointerException("您已开启 【禁用用户/账户】 功能，则响应给客户端的错误信息不为空");
            this.authHandler = authenticationManager;
            this.disabledResponseMessage = disabledResponseMessage;
        }
    }

    /* **************************************************************************************
     *
     *          需要子类实现的功能抽象方法
     *
     * **************************************************************************************
     * */

    @Note("获取子类缓存们自己默认的过期时间单位")
    protected abstract Timeunit protected_getImplSelfTimeunit();

    @Note("从缓存中获取已经请求的次数，注意这个缓存必须要有过期时间的")
    protected abstract Integer protected_getRequestedCount(String key);

    @Note("set 已请求的次数进缓存里，并设置过期时间")
    protected abstract void protected_setRequestedCount(String key, int count, long expiration, Timeunit timeunit);

    @Note("从缓存中获取该 key 的剩余时间")
    protected abstract long protected_getRemainder(String key);

    @Note("从缓存中移除已请求的次数限制")
    protected abstract void protected_removeRequestCount(String key);

    /* **************************************************************************************
     *
     *          拦截 + 校验逻辑
     *
     * **************************************************************************************
     * */

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(handler instanceof ResourceHttpRequestHandler) return true;
        HandlerMethod hm = (HandlerMethod) handler;
        Method method = hm.getMethod();
        RequestLimit limit = method.getAnnotation(RequestLimit.class);
        String clientIP = WebKit.getClientIP();
        //如果方法上没有注解，那就看看类上是否有，有就用类的，没有就说明不限流，直接放行
        if(limit == null) {
            Class<?> typeClass = method.getDeclaringClass();
            RequestLimit typeLimit = typeClass.getAnnotation(RequestLimit.class);
            return typeLimit == null || private_checkIsAllow(typeLimit, clientIP, request, response);
        }
        return private_checkIsAllow(limit, clientIP, request, response);
    }


    /* **************************************************************************************
     *
     *          私有逻辑
     *
     * **************************************************************************************
     * */

    @Note("校验是否可以访问接口，同时判断是否开启了封号功能，若开启了且阈值达到了就执行封号逻辑")
    private boolean private_checkIsAllow(RequestLimit limit, String clientIP, HttpServletRequest request, HttpServletResponse response) throws Exception {
        long time = limit.time();
        Timeunit timeunit = limit.timeunit();
        String key = this.cachePrefix + clientIP + ':' + request.getRequestURI();
        Integer requestedCount = this.protected_getRequestedCount(key);
        if(requestedCount == null) {
            //还没请求过，这是第一次，set 进缓存里，设置注解那里配置的过期时间
            this.protected_setRequestedCount(key, 1, time, timeunit);
            return true;
        }
        int allow = limit.allow();
        //最大允许请求次数，超过这个阈值就要封号
        int maxAllow = limit.maxAllow();
        if(maxAllow >= allow && requestedCount >= maxAllow && this.authHandler != null) {
            this.authHandler.handleDisable(request);
            this.protected_removeRequestCount(key);
            response.getWriter().write(this.private_getDisabledErrorThrowMsg(limit));
            return false;
        }
        //刚好限制过期，重新按第一次计算，set 进缓存
        long ttl = this.protected_getRemainder(key);
        if(ttl < 1) {
            this.protected_setRequestedCount(key, 1, time, timeunit);
            return true;
        }
        //否则，若已请求的 >= 可允许请求的。拒绝访问并响应提示信息
        if(requestedCount >= allow) {
            this.protected_setRequestedCount(key, requestedCount, ttl, this.protected_getImplSelfTimeunit());
            response.getWriter().write(this.private_getErrorThrowMsg(limit));
            return false;
        }
        //都没有，那就正常放行，并让请求数 + 1
        this.protected_setRequestedCount(key, (requestedCount + 1), ttl, this.protected_getImplSelfTimeunit());
        return true;
    }


    @Note("获取注解自己配置的错误返回信息，如果是默认值那就返回 null")
    private String private_getErrorThrowMsg(RequestLimit limit) {
        int code = limit.code();
        String msg = limit.msg();
        if(code <= 0 || msg.isEmpty()) {
            return this.limitResponseMessage;
        }
        return "{\"code\":" + code + ", \"message\":\"" + msg + "\"}";
    }

    @Note("获取注解自己配置的错误返回信息，如果是默认值那就返回 null")
    private String private_getDisabledErrorThrowMsg(RequestLimit limit) {
        int code = limit.dCode();
        String msg = limit.dMsg();
        if(code <= 0 || msg.isEmpty()) {
            return this.disabledResponseMessage;
        }
        return "{\"code\":" + code + ", \"message\":\"" + msg + "\"}";
    }

}
